#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""Predesys Server - Core - Services management
@version: 0.2.5
@license: GPL 3.0 (see "license" file)
@author: Jose A. Jimenez
@contact: jajimc@gmail.com"""

import sys
sys.dont_write_bytecode = True
import os
import os.path
DIR = os.path.abspath(os.path.dirname(os.path.realpath(__file__)))
sys.path.append(os.path.join(DIR, "../errorlog"))
import errorlog
import security
import coreexceptions
import sqlalchemy
import data
import users

class ServiceType():
    def __init__(self, id, name):
        self.set_id(id)
        self.set_name(name)
    
    def get_id(self):
        return self._id
            
    def get_name(self):
        return self._name
    
    def set_id(self, id):
        if id == None or (type(id) != str and type(id) != unicode) or id == "": raise coreexceptions.ValueNotValidException("'id' must be a not null and not empty string")
        self._id = id
            
    def set_name(self, name):
        if name != None and (type(name) != str and type(name) != unicode): raise coreexceptions.ValueNotValidException("'name' must a string or null")
        
        if name == "": self.name = None
        else: self._name = name

class Service:
    def __init__(self, id, version, serviceType, authenticated, enabled, path, name, author):
        self.set_id(id)
        self.set_version(version)
        self.set_type(serviceType)
        self.set_authenticated(authenticated)
        self.set_enabled(enabled)
        self.set_path(path)
        self.set_name(name)
        self.set_author(author)
        
    def get_id(self):
        return self._id
        
    def get_version(self):
        return self._version
        
    def get_type(self):
        return self._serviceType
        
    def is_authenticated(self):
        return self._authenticated
        
    def is_enabled(self):
        return self._enabled
        
    def get_path(self):
        return self._path
        
    def get_name(self):
        return self._name
        
    def get_author(self):
        return self._author
               
    def set_id(self, id):
        if id == None or (type(id) != str and type(id) != unicode) or id == "": raise coreexceptions.ValueNotValidException("'id' must be a not null and not empty string")
        self._id = id
        
    def set_version(self, version):
        if version != None and (type(version) != str and type(version) != unicode): raise coreexceptions.ValueNotValidException("'version' must be a string or null")
        
        if version == "": self.version = None
        else: self._version = version
        
    def set_type(self, serviceType):
        if serviceType != None and not isinstance(serviceType, ServiceType): raise coreexceptions.ValueNotValidException("'serviceType' must be an ServiceType object or null")
        self._serviceType = serviceType
        
    def set_authenticated(self, authenticated):
        if authenticated == None or type(authenticated) != bool: raise coreexceptions.ValueNotValidException("'authenticated' must be a not null boolean")
        self._authenticated = authenticated
        
    def set_enabled(self, enabled):
        if enabled == None or type(enabled) != bool: raise coreexceptions.ValueNotValidException("'enabled' must be a not null boolean")
        self._enabled = enabled
        
    def set_path(self, path):
        if path == None or (type(path) != str and type(path) != unicode) or path == "": raise coreexceptions.ValueNotValidException("'path' must be a not null and not empty string")
        self._path = path
        
    def set_name(self, name):
        if name != None and (type(name) != str and type(name) != unicode): raise coreexceptions.ValueNotValidException("'name' must be a string or null")
        
        if name == "": self.name = None
        else: self._name = name
        
    def set_author(self, author):
        if author != None and (type(author) != str and type(author) != unicode): raise coreexceptions.ValueNotValidException("'author' must be a string or null")
        self._author = author

        if author == "": self.author = None
        else: self._author = author
        
def get_service_types():
    """Gets every service type.
    @return: List of service types
    @rtype: C{list}"""
    serviceTypes = []
    
    try:
        dbConnection = data.open_connection()
        statement = "select id from service_types;"
        result = data.execute(dbConnection, statement)
        for row in result:
            serviceTypes.append(row["id"])
        result.close()
        data.close_connection(dbConnection)
    except Exception as exc:
        errorlog.log_exception(exc.args[0], "predesys-server.core.services.get_service_types")
        raise
        
    return serviceTypes
    
def is_service_type(id):
    """Gets if a string corresponds to a registered service type.
    @param id: Service type ID
    @type id: C{str}
    @return: *True* if *id* corresponds to a registered service type. *False* in other case.
    @rtype: C{bool}"""
    if id == None or (type(id) != str and type(id) != unicode) or id == "" : raise coreexceptions.ValueNotValidException("'id' must be a valid string")

    try:
        dbConnection = data.open_connection()
        statement = "select id from service_types where id='%s';" % id
        result = data.execute(dbConnection, statement)
        row = result.fetchone()
        result.close()
        data.close_connection(dbConnection)
    except Exception as exc:
        errorlog.log_exception(exc.args[0], "predesys-server.core.services.is_service_type")
        raise
    
    return (row != None)

def get_service_type_data(id):
    """Gets a service type data.
    @param id: Service type ID
    @type id: C{str}
    @return: Service type data
    @rtype: L{ServiceType}"""
    if id == None or (type(id) != str and type(id) != unicode) or id == "": raise coreexceptions.ValueNotValidException("'id' must be a not null and not empty string")
    if not is_service_type(id): raise coreexceptions.SystemException("'%s' is not a service type" % id)
    
    serviceTypeData = None
    
    try:
        dbConnection = data.open_connection()
        statement = "select * from service_types where id='%s';" % id
        result = data.execute(dbConnection, statement)
        row = result.fetchone()
        serviceTypeData = ServiceType(row["id"], row["name"])
        result.close()
        data.close_connection(dbConnection)
    except Exception as exc:
        errorlog.log_exception(exc.args[0], "predesys-server.core.services.get_service_type_data")
        raise
        
    return serviceTypeData
    
def get_services():
    """Gets every service.
    @return: List of services
    @rtype: C{list}"""
    services = []
    
    try:
        dbConnection = data.open_connection()
        statement = "select id from services;"
        result = data.execute(dbConnection, statement)
        for row in result:
            services.append(row["id"])
        result.close()
        data.close_connection(dbConnection)
    except Exception as exc:
        errorlog.log_exception(exc.args[0], "predesys-server.core.services.get_services")
        raise
        
    return services
    
def get_services_by_type(serviceTypeId):
    """Gets every service of a given type.
    @param serviceTypeId: Service type ID
    @type serviceTypeId: C{str}
    @return: List of services
    @rtype: C{list}"""
    if serviceTypeId == None or (type(serviceTypeId) != str and type(serviceTypeId) != unicode) or serviceTypeId == "":
        raise coreexceptions.ValueNotValidException("'serviceTypeId' must be a not null and not empty string")
    if not is_service_type(serviceTypeId): raise coreexceptions.SystemException("'%s' is not a service type" % serviceTypeId)
    
    services = []
    
    try:
        dbConnection = data.open_connection()
        statement = "select id from services where service_type_id='%s';" % serviceTypeId
        result = data.execute(dbConnection, statement)
        for row in result:
            services.append(row["id"])
        result.close()
        data.close_connection(dbConnection)
    except Exception as exc:
        errorlog.log_exception(exc.args[0], "predesys-server.core.services.get_services_by_type")
        raise
        
    return services
    
def get_enabled_services_by_type(serviceTypeId):
    """Gets every enabled service of a given type.
    @param serviceTypeId: Service type ID
    @type serviceTypeId: C{str}
    @return: List of services
    @rtype: C{list}"""
    if serviceTypeId == None or (type(serviceTypeId) != str and type(serviceTypeId) != unicode) or serviceTypeId == "":
        raise coreexceptions.ValueNotValidException("'serviceTypeId' must be a not null and not empty string")
    if not is_service_type(serviceTypeId): raise coreexceptions.SystemException("'%s' is not a service type" % serviceTypeId)
    
    services = []
    
    try:
        dbConnection = data.open_connection()
        statement = "select id from services where service_type_id='%s' and enabled=1;" % serviceTypeId
        result = data.execute(dbConnection, statement)
        for row in result:
            services.append(row["id"])
        result.close()
        data.close_connection(dbConnection)
    except Exception as exc:
        errorlog.log_exception(exc.args[0], "predesys-server.core.services.get_enabled_services_by_type")
        raise
        
    return services
    
def get_disabled_services_by_type(serviceTypeId):
    """Gets every disabled service of a given type.
    @param serviceTypeId: Service type ID
    @type serviceTypeId: C{str}
    @return: List of services
    @rtype: C{list}"""
    if serviceTypeId == None or (type(serviceTypeId) != str and type(serviceTypeId) != unicode) or serviceTypeId == "":
        raise coreexceptions.ValueNotValidException("'serviceTypeId' must be a not null and not empty string")
    if not is_service_type(serviceTypeId): raise coreexceptions.SystemException("'%s' is not a service type" % serviceTypeId)
    
    services = []
    
    try:
        dbConnection = data.open_connection()
        statement = "select id from services where service_type_id='%s' and enabled=0;" % serviceTypeId
        result = data.execute(dbConnection, statement)
        for row in result:
            services.append(row["id"])
        result.close()
        data.close_connection(dbConnection)
    except Exception as exc:
        errorlog.log_exception(exc.args[0], "predesys-server.core.services.get_disabled_services_by_type")
        raise
        
    return services
    
def is_service(id):
    """Gets if a string corresponds to a registered service.
    @param id: Service ID
    @type id: C{str}
    @return: *True* if *id* corresponds to a registered service. *False* in other case.
    @rtype: C{bool}"""
    if id == None or (type(id) != str and type(id) != unicode) or id == "" : raise coreexceptions.ValueNotValidException("'id' must be a valid string")

    try:
        dbConnection = data.open_connection()
        statement = "select id from services where id='%s';" % id
        result = data.execute(dbConnection, statement)
        row = result.fetchone()
        result.close()
        data.close_connection(dbConnection)
    except Exception as exc:
        errorlog.log_exception(exc.args[0], "predesys-server.core.services.is_service")
        raise
    
    return (row != None)

def get_service_data(id):
    """Gets a service data.
    @param id: Service ID
    @type id: C{str}
    @return: Service data
    @rtype: L{Service}"""
    if id == None or (type(id) != str and type(id) != unicode) or id == "": raise coreexceptions.ValueNotValidException("'id' must be a not null and not empty string")
    if not is_service(id): raise coreexceptions.SystemException("'%s' is not a service" % id)
    
    serviceData = None
    
    try:
        dbConnection = data.open_connection()
        statement = "select * from services where id='%s';" % id
        result = data.execute(dbConnection, statement)
        row = result.fetchone()   
        if row != None:
            if row["service_type_id"] != None and row["service_type_id"] != "": serviceType = ServiceType(row["service_type_id"], None)
            else: serviceType = None
            
            serviceData = Service(row["id"], row["version"], serviceType, bool(int(row["authenticated"])), bool(int(row["enabled"])), row["file_path"], row["name"], row["author"])
        result.close()
        data.close_connection(dbConnection)
    except Exception as exc:
        errorlog.log_exception(exc.args[0], "predesys-server.core.services.get_service_data")
        raise
        
    return serviceData
    
def add_service(serviceData):
    """Adds a new service to the system.
    @param serviceData: Service to add
    @type serviceData: L{Service}"""
    security.check_root()
    if serviceData == None or not isinstance(serviceData, Service): raise coreexceptions.ValueNotValidException("'serviceData' must be a not null Service object")
    id = serviceData.get_id()
    if is_service(id): raise coreexceptions.SystemException("'%s' already exists" % id)
    
    version = serviceData.get_version()
    if (version == None or version == ""): version = "NULL"
    else: version = "'%s'" % version
    
    serviceType = serviceData.get_type()
    if (serviceType == None):
        serviceTypeId = "NULL"
    else:
        serviceTypeId = serviceType.get_id()
        if not is_service_type(serviceTypeId): raise coreexceptions.SystemException("'%s' is not a service type" % serviceTypeId) 
        serviceTypeId = "'%s'" % serviceTypeId
        
    if serviceData.is_authenticated(): authenticated = 1
    else: authenticated = 0
    
    if serviceData.is_enabled(): enabled = 1
    else: enabled = 0
    
    path = serviceData.get_path()
    if (path == None or path == ""): path = "NULL"
    else: path = "'%s'" % path
    
    name = serviceData.get_name()
    if (name == None or name == ""): name = "NULL"
    else: name = "'%s'" % name
    
    author = serviceData.get_author()
    if (author == None or author == ""): author = "NULL"
    else: author = "'%s'" % author
    
    try:
        dbConnection = data.open_connection()
        statement = "insert into services values ('%s', %s, %s, %s, %s, %s, %s, %s);" % (id, version, serviceTypeId, authenticated, enabled, path, name, author)
        result = data.execute(dbConnection, statement)
        result.close()
        data.close_connection(dbConnection)
    except Exception as exc:
        errorlog.log_exception(exc.args[0], "predesys-server.core.services.add_service")
        raise

def modify_service(oldId, newId, version, serviceTypeId, path, name, author):
    """Modifies an existing service.
    @param oldId: Current service ID
    @type oldId: C{str}
    @param newId: Service new ID
    @type newId: C{str}
    @param version: Service new version
    @type version: C{str}
    @param serviceTypeId: Service new type ID
    @type serviceTypeId: C{str}
    @param path: Service new file path
    @type path: C{str}
    @param name: Service new name
    @type name: C{str}
    @param author: Service new author
    @type author: C{str}"""
    security.check_root()
    if oldId == None or type(oldId) != str or oldId == "" : raise coreexceptions.ValueNotValidException("'oldId' must be a not null and not empty string")
    if newId == None or type(newId) != str or newId == "" : raise coreexceptions.ValueNotValidException("'newId' must be a not null and not empty string")
    if version != None and type(version) != str: raise coreexceptions.ValueNotValidException("'version' must be a valid string")
    if serviceTypeId != None and (type(serviceTypeId) != str or serviceTypeId == ""): raise coreexceptions.ValueNotValidException("'serviceTypeId' must be a not empty string or null")
    if path != None and type(path) != str: raise coreexceptions.ValueNotValidException("'path' must be a valid string")
    if name != None and type(name) != str: raise coreexceptions.ValueNotValidException("'name' must be a valid string")
    if author != None and type(author) != str: raise coreexceptions.ValueNotValidException("'author' must be a valid string")  
    if get_service_data(oldId).get_type().get_id() == "core-callable": raise coreexceptions.ValueNotValidException("'%s' is a Core service, it can not be modified or deleted" % oldId)
    
    if not is_service(oldId): raise coreexceptions.SystemException("'%s' is not a service" % oldId)
    if oldId != newId and is_service(newId): raise coreexceptions.SystemException("'%s' already exists" % newId)
    
    if (version == None or version == ""): version = "NULL"
    else: version = "'%s'" % version
    
    if serviceTypeId != None and not is_service_type(serviceTypeId): raise coreexceptions.SystemException("'%s' is not a service type" % serviceTypeId)
    if serviceTypeId == None or serviceTypeId == "":
        serviceTypeId = "NULL"
    else:
        serviceTypeId = "'%s'" % serviceTypeId
    
    if (path == None or path == ""): path = "NULL"
    else: path = "'%s'" % path
    
    if (name == None or name == ""): name = "NULL"
    else: name = "'%s'" % name
    
    if (author == None or author == ""): author = "NULL"
    else: author = "'%s'" % author
    
    try:
        dbConnection = data.open_connection()
        statement = "update services set id='%s', version=%s, service_type_id=%s, file_path=%s, name=%s, author=%s where id='%s';" % (newId, version, serviceTypeId, path, name, author, oldId)
        result = data.execute(dbConnection, statement)
        result.close()
        data.close_connection(dbConnection)
    except Exception as exc:
        errorlog.log_exception(exc.args[0], "predesys-server.core.services.modify_service")
        raise

def authenticate_service(id, authenticated):
    """Enables/disables user authentication in a service.
    @param id: Service ID
    @type id: C{str}
    @param authenticated: If *True*, user authentication will be enabled; disabled in other case.
    @type authenticated: C{bool}"""
    security.check_root()
    if id == None or type(id) != str or id == "": raise coreexceptions.ValueNotValidException("'id' must be a not null and not empty string")
    if authenticated == None or type(authenticated) != bool: raise coreexceptions.ValueNotValidException("'authenticated' must be a not null boolean")
    if not is_service(id): raise coreexceptions.SystemException("'%s' is not a service" % id)
    
    if authenticated: authenticated = 1
    else: authenticated = 0
    
    try:
        dbConnection = data.open_connection()
        statement = "update services set authenticated=%s where id='%s';" % (authenticated, id)
        result = data.execute(dbConnection, statement);
        result.close()
        data.close_connection(dbConnection)
    except Exception as exc:
        errorlog.log_exception(exc.args[0], "predesys-server.core.services.authenticate_service")
        raise

def enable_service(id, enabled):
    """Enables/disables a service.
    @param id: Service ID
    @type id: C{str}
    @param enabled: If *True*, enabled will be enabled; disabled in other case.
    @type enabled: C{bool}"""
    security.check_root()
    if id == None or type(id) != str or id == "": raise coreexceptions.ValueNotValidException("'id' must be a not null and not empty string")
    if enabled == None or type(enabled) != bool: raise coreexceptions.ValueNotValidException("'enabled' must be a not null boolean")
    if not is_service(id): raise coreexceptions.SystemException("'%s' is not a service" % id)
    
    if enabled: enabled = 1
    else: enabled = 0
    
    try:
        dbConnection = data.open_connection()
        statement = "update services set enabled=%s where id='%s';" % (enabled, id)
        result = data.execute(dbConnection, statement);
        result.close()
        data.close_connection(dbConnection)
    except Exception as exc:
        errorlog.log_exception(exc.args[0], "predesys-server.core.services.enable_service")
        raise

def delete_service(id):
    """Deletes a service.
    @param id: Service ID
    @type id: C{str}"""
    security.check_root()
    if id == None or (type(id) != str and type(id) != unicode) or id == "": raise coreexceptions.ValueNotValidException("'id' must be a not null and not empty string")
    if not is_service(id): raise coreexceptions.SystemException("'%s' is not a service" % id) 
    serviceType = get_service_data(id).get_type()
    if serviceType != None and serviceType.get_id() == "core-callable": raise coreexceptions.ValueNotValidException("'%s' is a Core service, it can not be modified or deleted" % id)
        
    try:        
        dbConnection = data.open_connection()
        statement = "delete from services where id='%s';" % id
        result = data.execute(dbConnection, statement)
        result.close()
        data.close_connection(dbConnection)
    except Exception as exc:
        errorlog.log_exception(exc.args[0], "predesys-server.core.services.delete_service")
        raise

def run_callable_service(id, args=None, userId=None, userPassword=None):
    """Runs a callable service and returns its result.
    @param id: Service ID
    @type id: C{str}
    @param args: Service input arguments
    @type args: C{list}
    @param userId: User ID to authenticate (if service requires it)
    @type userId: C{str}
    @param userPassword: User password to authenticate (if service requires it)
    @type userPassword: C{str}
    @return: Service result
    @rtype: C{str}"""
    if id == None or (type(id) != str and type(id) != unicode) or id == "":
        raise coreexceptions.ValueNotValidException("'id' must be a not null and not empty string")
        
    if args != None and type(args) != list:
        raise coreexceptions.ValueNotValidException("'args' must be a list")
        
    notLogin = (userId == None and userPassword == None)
    login1 = ( (type(userId) == str or type(userId) == unicode) and userId != "" )
    login2 = ( (type(userId) == str or type(userId) == unicode) and userId != "" )
    login = (login1 and login2)
        
    if not (notLogin or login):
        raise coreexceptions.ValueNotValidException("Login user data bad format")
        
    if not is_service(id):
        raise coreexceptions.SystemException("'%s' is not a service" % id)
        
    service = get_service_data(id)
    serviceType = service.get_type()
    
    if serviceType == None:
        raise coreexceptions.SystemException("'%s' is not a callable service, it can not be executed" % id)
    else:
        serviceTypeId = serviceType.get_id()
        if serviceTypeId != "callable-system" and serviceTypeId != "callable-user":
            raise coreexceptions.SystemException("'%s' is not a callable service, it can not be executed" % id)
        
    if not service.is_enabled():
        raise coreexceptions.ValueNotValidException("'%s' service is disabled, it can not be executed" % id)
        
    if service.is_authenticated():
        if not login:
            raise coreexceptions.SystemException("'%s' is an authenticated service. Authentication data not provided." % id)
        else:
            if not (security.check_user_identity(userId, userPassword) and users.is_enabled_user(userId)):
                raise coreexceptions.SystemException("Incorrect user/password")
            if not security.check_service_execution_permission(userId, id): 
                raise coreexceptions.SystemException("User '%s' do not have permission to run '%s'" % (userId, id))
    
    try:
        servicePath = service.get_path()
        
        if serviceTypeId == "callable-system":
            modulePath = os.path.abspath(os.path.join(DIR, "../services"))
        else:
            modulePath = os.path.abspath(os.path.dirname(servicePath))
        
        sys.path.append(modulePath)
        moduleName = os.path.basename(servicePath)
        moduleName = moduleName[:len(moduleName) - 3]
        module = __import__(moduleName)
        
        result = module.run(userId, args)
    except Exception as exc:
        errorlog.log_exception(exc.args[0], "predesys-server.core.services.run_callable_service")
        raise
    
    return result
        
def run_automatic_service(id, args=None):
    """Runs an automatic service and returns its result.
    @param id: Service ID
    @type id: C{str}
    @param args: Service input arguments
    @type args: C{list}
    @return: Service result
    @rtype: C{str}"""
    if id == None or (type(id) != str and type(id) != unicode) or id == "":
        raise coreexceptions.ValueNotValidException("'id' must be a not null and not empty string")
        
    if args != None and type(args) != list:
        raise coreexceptions.ValueNotValidException("'args' must be a list")
        
    if not is_service(id):
        raise coreexceptions.SystemException("'%s' is not a service" % id)
        
    service = get_service_data(id)
    serviceType = service.get_type()
    
    if serviceType == None:
        raise coreexceptions.SystemException("'%s' is not an automatic service, it can not be executed" % id)
    else:
        serviceTypeId = serviceType.get_id()
        if serviceTypeId != "automatic-user":
            raise coreexceptions.SystemException("'%s' is not an automatic service, it can not be executed" % id)
        
    if not service.is_enabled():
        raise coreexceptions.ValueNotValidException("'%s' service is disabled, it can not be executed" % id)
        
    try:
        servicePath = service.get_path()
        modulePath = os.path.abspath(os.path.dirname(servicePath))
        sys.path.append(modulePath)
        moduleName = os.path.basename(servicePath)
        moduleName = moduleName[:len(moduleName) - 3]
        module = __import__(moduleName)
        
        result = module.run(args)
    except Exception as exc:
        errorlog.log_exception(exc.args[0], "predesys-server.core.services.run_automatic_service")
        raise
    
    return result
        
def get_formatted_message_list_header():
    """@return: Formatted message list header
    @rtype: C{str}"""
    return "<predesys_message_list>"
    
def get_formatted_message(destination, body):
    """@param destination: Message destination. It's a MAC address.
    @type destination: C{str}
    @param body: Message body
    @type body: C{str}
    @return: Formatted message
    @rtype: C{str}"""
    if destination == None or (type(destination) != str and type(destination) != unicode) or destination == "":
        raise coreexceptions.ValueNotValidException("'destination' must be a not null and not empty string")
    if body == None or (type(body) != str and type(body) != unicode) or body == "":
        raise coreexceptions.ValueNotValidException("'body' must be a not null and not empty string")
        
    return "<message><destination>%s</destination><body>%s</body></message>" % (destination, body)
    
def get_formatted_message_list_footer():
    """@return: Formatted message list footer
    @rtype: C{str}"""
    return "</predesys_message_list>"
    
def is_message_list(messageListString):
    """Gets if a string corresponds to a valid list of messages.
    @param messageListString: Message list
    @type messageListString: C{str}
    @return: *True* if string is a valid message list; *False* in other case.
    @rtype: C{bool}"""
    if messageListString == None or (type(messageListString) != str and type(messageListString) != unicode) or messageListString == "": return False
    
    header = get_message_list_header()
    footer = get_formatted_message_list_footer()
    return (text[:len(header)] == header and text[len(text)-len(footer):] == footer)
    
def is_message(text):
    """Gets if a string corresponds to a valid message.
    @param text: Message
    @type text: C{str}
    @return: *True* if string is a valid message; *False* in other case.
    @rtype: C{bool}"""
    if text == None or (type(text) != str and type(text) != unicode) or text == "": return False
    return (text[:22] == "<message><destination>" and text[len(text)-17:] == "</body></message>")
